﻿using System;
using log4net;
using NCommet.Core.Agents;
using System.Collections.Specialized;
using NCommet.Core;
using NCommet.Core.Services;
using System.Configuration;

namespace NCommet.Modules.Authorization
{
    public class AMCachedImpl : AuthorizationManagerImpl
    {
        private static readonly ILog log = LogManager.GetLogger("AMCachedImpl");

        struct CacheKey
        {
            private int itemId;

            public int ItemId
            {
                get { return itemId; }
            }

            private string role;

            public string Role
            {
                get { return role; }
            }

            public CacheKey(int ItemId, string Role, AccessLevel AccessLevel)
            {
                this.itemId = ItemId;
                this.role = Role;
                this.accessLevel = AccessLevel;
                this.createdOn = DateTime.Now;
            }

            public CacheKey(int ItemId, string Role) : this(ItemId, Role, AccessLevel.None) { }

            public override bool Equals(object obj)
            {
                return obj is CacheKey && this == (CacheKey)obj;
            }

            public override int GetHashCode()
            {
                return itemId * 17 + role.GetHashCode();
            }

            public static bool operator ==(CacheKey x, CacheKey y)
            {
                // If both are null, or both are same instance, return true.
                if (System.Object.ReferenceEquals(x, y))
                {
                    return true;
                }

                // If one is null, but not both, return false.
                if (((object)x == null) || ((object)y == null))
                {
                    return false;
                }

                // different references
                return x.itemId == y.itemId && x.role == y.role;
            }

            public static bool operator !=(CacheKey x, CacheKey y)
            {
                return !(x == y);
            }

            private DateTime createdOn;

            public DateTime CreatedOn
            {
                get { return createdOn; }
            }

            private AccessLevel accessLevel;

            public AccessLevel AccessLevel
            {
                get { return accessLevel; }
            }

            public TimeSpan Age
            {
                get { return DateTime.Now.Subtract(createdOn); }
            }

            public override string ToString()
            {
                return string.Format("Id = {0}, Role = {1}, AL = {2}, Age = {3}", itemId, role, accessLevel, Age);
            }
        }

        class Cache
        {
            private OrderedDictionary map = new OrderedDictionary();

            private object locker = new object();

            public bool Exists(int ItemId, string role)
            {
                lock (locker)
                {
                    return map.Contains(new CacheKey(ItemId, role));
                }
            }

            public void Set(int ItemId, string role, AccessLevel accessLevel)
            {
                lock (locker)
                {
                    CacheKey cd = new CacheKey(ItemId, role, accessLevel);
                    map.Remove(cd);
                    map.Add(cd, cd);
                    Purge();
                }
            }

            public void Remove(int ItemId, string role)
            {
                lock (locker)
                {
                    map.Remove(new CacheKey(ItemId, role));
                }
            }

            public void Remove(int ItemId)
            {
                lock (locker)
                {
                    map.Clear();
                }
            }

            public void Remove(string role) { lock (locker) { map.Clear(); } }

            public AccessLevel Get(int ItemId, string role)
            {
                lock (locker)
                {
                    CacheKey ck = new CacheKey(ItemId, role);
                    AccessLevel result = ((CacheKey)map[ck]).AccessLevel;
                    // purge
                    Purge();
                    return result;
                }
            }

            private void Purge()
            {
                DateTime dt = DateTime.Now;

                while (map.Count > 0 && (map.Count > MaxDictionarySize || dt > ((CacheKey)map[0]).CreatedOn.Add(MaxAge)))
                {
                    CacheKey cd = (CacheKey)map[0];
                    log.DebugFormat("Purge. Item={0}, Role={1}. Reason={2}. Age={3}.", cd.ItemId, cd.Role, map.Count > 1000 ? "Size" : "Age",
                        cd.Age
                        );
                    map.RemoveAt(0);
                }
                log.DebugFormat("Cache state. Dictionary size: {0}.", map.Count);
            }

            public Cache(int MaxDictionarySize, TimeSpan MaxAge)
            {
                this.maxDictionarySize = MaxDictionarySize;
                this.maxAge = MaxAge;
            }

            private int maxDictionarySize;

            public int MaxDictionarySize
            {
                get { return maxDictionarySize; }
                set { maxDictionarySize = value; }
            }

            private TimeSpan maxAge;

            public TimeSpan MaxAge
            {
                get { return maxAge; }
                set { maxAge = value; }
            }


        }

        private Cache cache;

        public AMCachedImpl()
        {
            int maxDictionarySize;

            try
            {
                maxDictionarySize = Math.Max(1000,
                    Convert.ToInt32(ConfigurationManager.AppSettings["AMCachedImpl.MaxDictionarySize"]));
            }
            catch
            {
                maxDictionarySize = 1000;
            }

            int maxAgeInMinutes;

            try
            {
                maxAgeInMinutes = Math.Max(10, Convert.ToInt32(ConfigurationManager.AppSettings["AMCachedImpl.MaxAgeMinutes"]));
            }
            catch
            {
                maxAgeInMinutes = 10;
            }


            cache = new Cache(maxDictionarySize, new TimeSpan(0, maxAgeInMinutes, 0));
        }

        public override AccessLevel GetAccessLevel(Item item, string role)
        {
            if (item == null)
                return AccessLevel.None;

            try
            {
                AccessLevel result;
                if (!cache.Exists(item.Id, role))
                {
                    log.DebugFormat("GetAccessLevel. Item={0}, role={1}. Asking DB.", item.Id, role);
                    cache.Set(item.Id, role, result = base.GetAccessLevel(item, role));
                }
                else
                {
                    log.DebugFormat("GetAccessLevel. Item={0}, role={1}. Asking Cache.", item.Id, role);
                    result = cache.Get(item.Id, role);
                }
                return result;
            }
            catch (Exception ex)
            {
                log.Error("GetAccessLevel", ex);
                throw ex;
            }
        }

        public override void ResetAccessLevel(Item item, string role)
        {
            try
            {
                base.ResetAccessLevel(item, role);
                log.DebugFormat("ResetAccessLevel {0} {1}", item.Id, role);
                cache.Remove(item.Id, role);
            }
            catch (Exception ex)
            {
                log.Error("ResetAccessLevel", ex);
                throw ex;
            }
        }

        public override void RolesDeleted(string[] roles)
        {
            try
            {
                base.RolesDeleted(roles);
                log.DebugFormat("RolesDeleted, role count={1}", roles.Length);
                foreach (string role in roles)
                    cache.Remove(role);
            }
            catch (Exception ex)
            {
                log.Error("RolesDeleted", ex);
                throw ex;
            }
        }

        public override void SetAccessLevel(Item item, string role, AccessLevel accessLevel)
        {
            try
            {
                base.SetAccessLevel(item, role, accessLevel);
                cache.Set(item.Id, role, accessLevel);
            }
            catch (Exception ex)
            {
                log.Error("SetAccessLevel", ex);
                throw ex;
            }
        }

        protected override void Instance_ItemDeleted(object sender, ItemEventArgs e)
        {
            try
            {
                base.Instance_ItemDeleted(sender, e);
                log.DebugFormat("Item Deleted {0}", e.Item.Id);
                cache.Remove(e.Item.Id);
            }
            catch (Exception ex)
            {
                log.Error("ItemDeleted", ex);
                throw ex;
            }
        }
    }
}
